By utilizing the shared library mechanism, LScript allows you to write shared libraries can be used to implement user-defined Object Agents. What this means is that new Object Agents (technically, these are referred to as Class Definitions) can be written within shared libraries, and instances of these Object Agents (Class Definitions) can be created in an LScript by a programmer utilizing the following declaration:
use "<filename>.dll" as class <ClassName>;The protocol for creating your own Object Agents is much more complex than that of simply creating a LScript-callable shared-library function. Because we have taken pains to emulate C++ class design, Object Agent shared libraries will contain "methods" (functions) that will only be accessible by the LScript engine (such as destructors), along with the normal "public" items that an Object Agent makes available to LScripts, such as data members and exported methods.
To better illustrate the requirements of building a user-defined Object Agent, we will construct a questionably-useful Object Agent called BobObj. BobObj will have only one "public" method, stradd(), which will concatenate two character strings together, returning the new string as its only return value.
As with DLL functions, topics covered in this section are of a programmatic content and are non-trivial in nature. They are intended for more advanced developers.
The location where these global values reside varies across LightWave 3D platforms. Users of LScript v1.3 or higher can utilize the globalstore() and globalrecall() functions to place values into the LScript-global areas. If, however, you are using versions of LScript prior to this, you must enter values manually.
HKEY_CURRENT_USER\Software\NewTek\LScriptUnder this key are other keys that are related directly to the specific LightWave plug-in architectures that LScript supports--LScript/IA, LScript/DM, LScript (Modeler), etc. It is within these individual architecture keys that the store() and recall() script functions place/retreive their values.
To make the 'LibraryPath' setting available to all LScript architectures (recommended),
you would create a new string value in the key mentioned above. This new
string value will have the identifier LibraryPath
, and its value will
be the path to the directory wherein ObjectAgent libraries and script insert files
can be found.
If you wish or need to store such files in more than one location, you can place up to 10 directory paths into the string value for LScript to search through. Each path must be separated from the others by a semi-colon (much the same as the MS-DOS PATH value).
Name Data (Default) (value not set) LibraryPath "f:\lw\dev\lscript\dll;f:\oa;c:\temp"
LScript on the DEC Alpha creates configuration files for each individual architecture. These configuration files are simple ASCII text, and can be edited by any text editor. Typically, these configuration files reside in the LightWave program directory (NewTek\Programs). The format of these configuration files is identical to the Windows INI file format. When setting the 'LibraryPath' value for an architecture, you will want to place it into the [LScript] section of the configuration file.
[LScript] ScriptPath=c:\bob\lscript\examples LibraryPath=c:\bob\lscript\ObjectAgent
A new structure is defined that houses an instance of the LSFunc structure discussed in Appendix A on DLL functions, along with several other data pointers:
typedef struct _lsdll { const char *ClassName; const char *InstanceName; LS_VAR *instance[MAXINSTANCE]; LSFunc *func; } LSDLL;The 'ClassName' member points to a character string that contains the Object Agent 'class' name, designated in the LScript.
'InstanceName' points to a character string that contains the variable name that contains the current instance of the Object Agent.
The 'instance' array is a series of MAXINSTANCE arrays of Object Agent instance data. The current instance can always access its own instance data, if there is any, through instance[0]. Although they are currently unused, the remaining elements are reserved for future instance data that may be available as a result of object inheritance. Instance data is discussed later in this section.
A new prototype has also been is defined for use in creating your "public" Object Agent methods. This prototype will be used by the LScript engine to invoke your methods:
typedef LS_VAR * (*ObjectAgentMethod)(int *,LS_VAR *,LSDLL *);
The four required methods are:
void constructor(count,variables,classinfo) 'count' is an integer value representing the number of parameters that were passed to this constructor 'variables' is a pointer to an LS_VAR array containing the values in the parameters that were passed to the constructor. it will be NULL (and 'count' will be 0) if no parameters were passed 'classinfo' is a pointer to an instance of an LSDLL structure (discussed earlier) that contains information about the class and instance names, pointers to internal LScript functions, and pointers to instance data void destructor(count,variables,classinfo) parameters are identical to those of the constructor, however because destructors cannot be invoked directly, 'count' will always be 0 and 'variables' will always be NULL boolean ownmethod(methodname) this interface method is used by LScript to see if an Object Agent instance owns the specified method. 'methodname' is a character string that names the method in question. ownmethod() should use this character string to compare to its known "public" methods, returning 'true' if one of its methods matches this name, or 'false' if the specified method is not available boolean owndata(dataname) this private method is similar to ownmethod(), but is intended to be applied to the "public" data members that the Object Agent makes available. 'dataname' is a character string that contains the name of the data member in questionIn addition, three more methods can exist in the interface. Two are used to manage access to "public" data members of the Object Agent Class, while the third locks the Object Agent into a specific revision of the Object Agent Interface (defined by the
OAMAJOR
and OAMINOR
declarators in the lscript.h
header file):
LS_VAR *returndata(dataname,classinfo) returndata() returns the value contained by the Object Agent data member named by 'dataname->extra', a void * that should be cast to a char *. returndata() will never be called for a data member unless owndata() has previously indicated that that data member is owned by this Object Agent the 'dataname->data.number' member should be cast to an integer value if the referenced data member is an array. 'dataname->data.number' is the index value used within the script, and should be converted to a zero-based offset. PLEASE NOTE that this value should only be cast if it is needed as an index value. Casting this value when it has not been explicitly set by the scripting engine can cause certain operating systems (notably, the DEC Alpha) to generate a Floating Point exception error. assigndata(dataname,data,classinfo) LScript will invoke this Object Agent method when a script needs to assign new data to a data member. LScript will handle, internally, any of the possible means of assigning new values to existing data members. this method will be passed the result, and be expected to update the appropriate data member. as with returndata(), 'dataname' is a pointer to an LS_VAR structure that houses both the data member identifier in the 'extra' area, and, if applicable, the requested index offset in the 'data.number' area if the data member can be indexed. 'data' is another LS_VAR pointer that contains the new value 'classinfo' is an instance of LSDLL. version(major,minor) 'major' and 'minor' are integer pointers that will receive the required major and minor revision values that indicate the level of the Interface that this Object Agent requires to function. If the returned values do not match the revision of the current Object Agent Interface specification supported by the plug-in, then the library will not be loaded and unresolved function references will likely result.A special behavior of the interface involves the existence of "public" data members in your Object Agent class. If your particular Object Agent makes available no "public" data members (i.e.,
owndata()
unconditionally
returns a 'false' value), then the returndata()
and
assigndata()
methods become optional, and need not be present in
your interface. The reason for this is that these two methods will never
be invoked by the LScript engine unless owndata()
returns
'true'.We will see examples of the usage of each of these methods later in this section when we write the BobObj Object Agent.
"Instance data" is essentially nothing more than a copy--or instance--of specific data values that an object must have in order to function. A good example of this need of instance data would be in a file-handling Object Agent. In our fictitious file-handling Object Agent, there will be methods defined that perform some type of specific functioning on the Object Agent data. Without the ability to handle instance data, you would have to maintain (and declare) a different Object Agent DLL for each FILE you wanted to handle as an object. Why? Because each DLL would have to have its own variables (declared at compile time) that it used to maintain items such as file name, FILE pointer, etc., but would only have one location where this information could be stored. If you needed to handle a new file using a single DLL, you would have to re-initialize an existing Object Agent with the new file information, or create a new instance of a declared Object Agent--both actions would have the net effect of destroying any previous data that was being maintained for that instance because it would have to be overwritten (remember, there is only ONE copy of the DLL loaded into memory, not one copy for each instance you create).
Another solution might be to have the individual Object Agent shared library provide its own mechanism for maintaining separate instance data. However, this means that Object Agent shared library writers would have to devise their own storage mechanisms instead of using the same established standard, and additional information might have to be provided to the shared library function so that they could distinguish among their potentially-many instances.
LScript employs the instdata() function for allowing an Object Agent shared library to create a new copy of the instance data that the Object Agent will need to function properly. The instance data created by instdata() is not required to be populated at the same time that it is created, because instdata(), if it exists, is always called before the Object Agent's constructor (which is the most appropriate place to initialize instance data values). If an Object Agent shared library does not contain an instdata() method, then LScript assumes that the Object Agent will have no instance data, and that particular value in the LSDLL structure (instance[0]) will be set to NULL.
If an Object Agent needs to maintain instance data, then the instdata() function should be present and designed to allocate the necessary storage locations. Each element of instance data is stored in a LS_VAR data wrapper, and passed back to LScript as a LS_VAR pointer. If you have more than one element, then you would need to allocate a linear array of LS_VAR data wrappers, and return a pointer to the first element to LScript after completing your processing. This same pointer returned by instdata() will be placed into the first element of the LSDLL structures instance array (instance[0]), which is passed to other Object Agent methods.
In the sample Object Agent we will create, we have no real need for maintaining instance data, but we do so for the sake of providing an example.
void version(int *maj,int *min) { *maj = 1; // need revision 1.1 *min = 1; // (this is the interface version, NOT the plug-in version) }Because we will maintain instance data--albeit, dummy data--in the BobObj Object Agent, we will begin with that method. Keep in mind that this method is optional.
In BobObj, we will maintain three instance values, named "tom," "dick" and "harry." These instance values are all considered "public", and all will be accessible by the host LScript. We will not actually have to name them thus internally, but rather we need to be aware that this will be how the script may reference them. Because we do not initialize the instance data in the instdata() method, it becomes quite simple:
LS_VAR * instdata(LSFunc *func) { LS_VAR *mydata; // we have three data members, all "public" mydata = (LS_VAR *)(*func->malloc)(3 * sizeof(LS_VAR)); return(mydata); }Here are some important things to be aware of regarding this method:
1. Do not use "static" variables to house your instance data. Static variables are single values that are shared between instances. While this may be your intention, more often than not you will want each instance to have its own unique set of data values from which to operate. For this reason, they must be allocated from the run-time heap. 2. You can structure instance data any way you wish, but no count is maintained by LScript of the number of entries in your instance data. It is assumed that the contents of the instance array are all known to the Object Agent, so access to the data in the array should NEVER exceed its boundaries. 3. Be sure to use the LScript-provided resource-management functions so that you don't introduce memory leaks to LightWave 3D. LScript tracks all allocations/releases of internal memory, and releases any remaining memory allocations when it terminates. If you use the standard library malloc(), LScript won't know about it and leaks could occur if you forget to explicitly free() it.If present, this will be the first method called by the LScript engine when a new instance is created in the script.
The next method we will complete is the Object Agent's constructor. The constructor is a required method, and it will be invoked whenever the script creates a new instance of the Object Agent. The constructor will always be called immediately following the invocation of instdata(), or it will be the first method invoked if instdata() does not exist.
In the BobObj constructor, we will initialize the instance data members that were created in instdata(). Constructors can be passed parameters by the script, and you will typically want to initialize your instance data using values that are derived from these parameters. We do not use constructor parameters in the BobObj Object Agent.
void constructor(int count,LS_VAR *parameters,LSDLL *lsdll) { LS_VAR *inst; if(count != 0) // error! we don't accept parameters { // (In C++: no class method matching this signature) (*lsdll->func->error)(...); return; } if((inst = lsdll->instance[0]) != NULL) // just be sure... { inst[0].data.number = 145.567; inst[1].data.string = "Dick"; // constant data member inst[2].data.vector[0] = 10.0; inst[2].data.vector[1] = 20.0; inst[2].data.vector[2] = 30.0; } }As mentioned earlier, an Object Agent's constructor is invoked at the time the instance is created in an LScript. Instances are created by the script programmer when the class name of the Object Agent is used as a function call. For instance, when we create instances of the BobObj Object Agent, we invoke the class constructor like so:
use "bobobj.dll" as class BobObj; ... main { ... bob1 = BobObj(); // create a BobObj instance ...Had we been required to pass arguments to the Object Agent constructor, we would have placed them within the parentheses:
bob1 = BobObj(1,"Hello"); // create an instance using parametersSome important things to note about an Object Agent's constructor method:
1. In the constructor, you should establish the appropriate data values in your instance data for this instance of the class. These values will usually be derived in some way from the parameters that are passed to this method. For example, were this some type of file- handling class, we would expect one of the parameters to indicate a unique filename the Object Agent would be expected to manage. 2. You can simulate method overloading in any Object Agent method simply by being flexible about the count and type of arguments provided to the method. You can then look for the patterns that match what those overloaded methods your Object Agent supports. Anything else would be a run-time error. 3. The 'InstanceName' attribute of the LSDLL structure has not yet been established when the Object Agent's constructor is invoked. The reason for this is that the instance being created has not yet been assigned to a variable at the point in time when the constructor is called. DO NOT USE IT!All well-written class definitions should include a destructor method. The destructor method is used to "clean up" the instance that is going away, or "out of scope." The bulk of this clean up is usually the graceful shutdown of any instance data members that require it, and freeing of the instance's resources (memory, files, etc.). It is within the destructor that we also release any resources allocated by the instdata() method.
Because the BobObj Object Agent we are creating manages no complex instance data, it's destructor is quite simple:
void destructor(int count,LS_VAR *parameters,LSDLL *lsdll) { if(lsdll->instance[0]) // free instdata()-allocated resources (*lsdll->func->free)(lsdll->instance[0]); }Of course, had we allocated additional resources within each instance data element, we would traverse each element in the destructor, invoking the appropriate LScript function to release it (free(), fclose(), etc.) before releasing the instance data array itself.
The next two required Object Agent methods are ownmethod() and owndata(). These two methods are quite straight-forward, in that they simply return a logical 'true' or 'false' to indicate the Object Agent's ability to service specific methods or data members. Our BobObj Object Agent sports one public method, "stradd()", and three public data members, "tom," "dick" and "harry."
int ownmethod(const char *method) // do we own a specific method? { if((method[0] == 's') && (strcmp(method,"stradd") == 0)) return(TRUE); return(FALSE); // we don't own this method... } int owndata(const char *data) // do we own a specific data member? { if((data[0] == 't') && (strcmp(data,"tom") == 0)) return(TRUE); else if((data[0] == 'd') && (strcmp(data,"dick") == 0)) return(TRUE); else if((data[0] == 'h') && (strcmp(data,"harry") == 0)) return(TRUE); return(FALSE); // we don't own this data member... }Notice in these two methods the lack of any error handling. When a method or data member is not found, neither method generates an error message of any kind. This is quite deliberate in design. We are allowing the LScript engine to decide how to handle this situation.
It is a possibility that in future revisions of this interface, the LScript engine will be able to support Object Agent inheritance, where one Object Agent can inherit the methods and data members of another, creating a new Object Agent all within the operation of the LScript. The reason we do not generate an error of some kind when we cannot locate a method or object method is that a negative return value from either of these methods could cause the LScript engine to query our parent(s) (or superclasses). In the current revision, however, the LScript engine simply generates an error condition on the Object Agent's behalf.
Probably the two methods with the greatest potential for size are the final required methods, returndata() and assigndata(). These methods are responsible for handling access to the Object Agent's public data members.
Returndata() is responsible for returning the value contained in a requested Object Agent data member. This method will not be called by the LScript engine unless the owndata() method has first been used to ensure that the data member to be accessed belongs to the Object Agent. Nevertheless, returndata() should always be prepared to handle situations where the provided data member identifier is not appropriate for the Object Agent. A return value of NULL indicates this error condition.
Here is the BobObj-version of returndata():
LS_VAR * returndata(LS_VAR *datamember,LSDLL *lsdll) { static LS_VAR lsvar; // assigned a different value on each call static LS_VAR *inst; static char *identifier; static int index; if(!lsdll->instance[0]) // no instance data, that's a problem return(NULL); inst = lsdll->instance[0]; // use some short-hand for easier reading identifier = (char *)datamember->extra; index = (int)datamember->data.number; if((identifier[0] == 't') && (strcmp(identifier,"tom") == 0)) { lsvar.type = LSNUMBER; lsvar.data.number = inst[0].data.number; } else if((identifier[0] == 'd') && (strcmp(identifier,"dick") == 0)) { lsvar.type = LSSTRING; lsvar.data.string = inst[1].data.string; } else if((identifier[0] == 'h') && (strcmp(identifier,"harry") == 0)) { lsvar.type = LSVECTOR; lsvar.data.vector[0] = inst[2].data.vector[0]; lsvar.data.vector[1] = inst[2].data.vector[1]; lsvar.data.vector[2] = inst[2].data.vector[2]; } else { // owndata() said we owned it, but we really don't (this should // never happen, but...) sprintf(errormsg,"bad data member name %s::%s.%s", lsdll->ClassName, lsdll->InstanceName, identifier); (*lsdll->func->error)(errormsg); // halt the script return(NULL); } return(&lsvar); }Assigndata() performs a function very similar to returndata(), however it updates data member values instead of returning them. It also performs a number of sanity checks on the value being used for the update to ensure data member integrity:
void assigndata(LS_VAR *datamember,LS_VAR *value,LSDLL *lsdll) { static LS_VAR *inst; static int i1,i2; static char *identifier; static int index; if((inst = lsdll->instance[0]) == NULL) // shorthand return; identifier = (char *)datamember->extra; index = (int)datamember->data.number; if((identifier[0] == 't') && (strcmp(identifier,"tom") == 0)) { if(value->type != LSINTEGER && value->type != LSNUMBER) { sprintf(errormsg, "invalid data type in data member assignment %s::%s.%s", lsdll->ClassName, lsdll->InstanceName, identifier); (*lsdll->func->error)(errormsg); return; } inst[0].data.number = value->data.number; } else if((identifier[0] == 'd') && (strcmp(identifier,"dick") == 0)) { // since we consider this to be a "constant" data member, any // assignment type would be illegal sprintf(errormsg, "illegal assignment to constant data member %s::%s.%s", lsdll->ClassName, lsdll->InstanceName, identifier); (*lsdll->func->error)(errormsg); } else if((identifier[0] == 'h') && (strcmp(identifier,"harry") == 0)) { if(value->type != LSVECTOR && value->type != LSINTEGER && value->type != LSNUMBER) { sprintf(errormsg, "invalid data type in data member assignment %s::%s.%s", lsdll->ClassName, lsdll->InstanceName, identifier); (*lsdll->func->error)(errormsg); return; } // we allow integer/number values to be used, as well as // vectors if(value->type == LSVECTOR) { inst[2].data.vector[0] = value->data.vector[0]; inst[2].data.vector[1] = value->data.vector[1]; inst[2].data.vector[2] = value->data.vector[2]; } else inst[2].data.vector[0] = inst[2].data.vector[1] = inst[2].data.vector[2] = value->data.number; } else { sprintf(errormsg,"bad data member name %s::%s.%s", lsdll->ClassName, lsdll->InstanceName, identifier); (*lsdll->func->error)(errormsg); } }Finally, to complete our BobObj Object Agent implementation, we need to create our single public method, stradd(). Notice that the public method stradd() adheres to the function prototype set forth in the 'lscript.h' header file for Object Agent "public" methods:
LS_VAR * stradd(int *count,LS_VAR *vars,LSDLL *lsdll) { static LS_VAR var; static char buf[512]; static char num[100]; static LS_VAR *inst; if(*count != 2) // check our "signature" return(NULL); *count = 0; // prepare to tell the engine how many return values if(vars[0].type == LSSTRING && vars[1].type == LSSTRING) { inst = lsdll->instance[0]; sprintf(num," (( <%g,%g,%g> ))", // this value will be appended inst[2].data.vector[0], // to each string as a visual inst[2].data.vector[1], // authentication of the instance inst[2].data.vector[2]); var.type = LSSTRING; if(strlen(vars[0].data.string) >= 512) strncpy(buf,vars[0].data.string,500); else strcpy(buf,vars[0].data.string); if((strlen(buf) + strlen(vars[1].data.string)) < 500) strcat(buf,vars[1].data.string); strcat(buf,num); var.data.string = buf; *count = 1; // tell engine that there is one element returned return(&var); } else return(NULL); }Public Object Agent methods have some operational points that need to be mentioned:
1. Notice that the first parameter to stradd(), used to indicate the number of script parameters that are contained in the 'vars' list, is a pointer instead of a value. The reason for this is that this location is used to indicate to the LScript engine the number of elements that are being returned from the method. In the case of stradd(), only one element is returned, so this value is set to 1 before returning (and initialized to 0 in case of error). 2. LScript is not aware of the arrangements you may have made within your code for the "containers" used to return values. You may have elected to use static values, or you may have allocated them from the heap. Because of this, the LScript engine cannot make assumptions concerning the disposition of your containers when it has finished processing their values (i.e., should it free them from the heap or simply ignore them?). For this reason, the method (or Object Agent) is solely responsible for allocating these items, and releasing them when they are no longer needed. Suggested practices for creating these resources might be to use a static array of LS_VARs, or to allocate the additional values needed in the Object Agent's instance data (instdata()). However, the use of static storage is the recommended method of returning values to the engine; allocating memory consumes critical resources and CPU time. If using static storage is not a viable option within your method, and you return the same number of items each time, a somewhat more elegant solution would be to allocate a static pointer in your method, and then allocate the needed elements using the provided LScript memory allocation function malloc() the first time the method is invoked. In this way, the memory would not be allocated until it is really needed (the method may not even be invoked throughout the entire script), and the Object Agent will not have to be concerned about not being able to release the memory because LScript will catch these unreleased resources when it terminates the script. stradd(...) { static LS_VAR *myvars = NULL; ... if(myvars == NULL) myvars = (LS_VAR *)(*lsdll->func->malloc) (sizeof(LS_VAR) * 5);
Once you have compiled your new Object Agent DLL, it must be placed into one of the locations in which the LScript engine will look when trying to resolve references. These locations were outlined in the previous section.
use "bobobj.dll" as class BobObj; bob0 = BobObj(); // a global instance main { bob1 = BobObj(); // two instances local to main() bob2 = BobObj(); info(bob1); // Object Agent instances can be passed info(bob2); // as parameters info(bob1.stradd("Hello","There")); info(bob2.stradd("I'm","Bob")); info(bob1.tom); info(bob1.dick); info(bob2.harry); info("calling test!"); test(); bob1.tom += 15; info(bob1.tom); bob2.harry /= 10; info(bob2.harry); test(); info(bob0.dick); info(bob0.tom++); // post-increment info(bob0.tom); // bob1 and bob2 destructor invoked upon exit from main() } // bob0 destructor invoked upon termination of script test { bob3 = BobObj(); // instance local to test() info(bob3.harry); // bob3 destructor is invoked upon exit from test() }The output from this LScript shows that the many instances of our BobObj Object Agent are all alive and well and functioning properly:
INFO: (ObjectAgent)BobObj::bob1 INFO: (ObjectAgent)BobObj::bob2 INFO: HelloThere (( <10,20,30> )) INFO: I'mBob (( <10,20,30> )) INFO: 145.567 INFO: Dick INFO: <10,20,30> INFO: calling test! INFO: <10,20,30> INFO: 160.567 INFO: <1,2,3> INFO: <10,20,30> INFO: Dick INFO: 145.567 INFO: 146.567